<?php

/*
 * This file is part of the php-phantomjs.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Ling\PhantomJs\Http;

use Ling\PhantomJs\Exception\InvalidMethodException;
use Ling\PhantomJs\Procedure\InputInterface;

/**
 * PHP PhantomJs
 *
 * @author Jon Wenmoth <contact@Ling.me>
 */
abstract class AbstractRequest
    implements RequestInterface, InputInterface
{
    /**
     * Headers
     *
     * @var array
     * @access protected
     */
    protected array $headers;

    /**
     * Settings
     *
     * @var array
     * @access protected
     */
    protected array $settings;

    /**
     * Cookies
     *
     * @var array
     * @access protected
     */
    protected array $cookies;

    /**
     * Request data
     *
     * @var array
     * @access protected
     */
    protected array $data;

    /**
     * Request URL
     *
     * @var string
     * @access protected
     */
    protected string $url;

    /**
     * Request method
     *
     * @var string
     * @access protected
     */
    protected string $method;

    /**
     * Timeout period
     *
     * @var int
     * @access protected
     */
    protected int $timeout;

    /**
     * Page load delay time.
     *
     * @var int
     * @access protected
     */
    protected int $delay;

    /**
     * Viewport width.
     *
     * @var int
     * @access protected
     */
    protected int $viewportWidth;

    /**
     * Viewport height.
     *
     * @var int
     * @access protected
     */
    protected int $viewportHeight;

    /**
     * Body styles.
     *
     * @var array
     * @access protected
     */
    protected array $bodyStyles;

    /**
     * Internal constructor
     *
     * @access public
     * @param string|null $url (default: null)
     * @param string $method (default: RequestInterface::METHOD_GET)
     * @param int $timeout (default: 5000)
     * @throws InvalidMethodException
     */
    public function __construct(string $url = null, string $method = RequestInterface::METHOD_GET, int $timeout = 5000)
    {
        $this->headers = array();
        $this->data = array();
        $this->bodyStyles = array();
        $this->settings = array();
        $this->delay = 0;
        $this->viewportWidth = 0;
        $this->viewportHeight = 0;

        $this->cookies = array(
            'add' => array(),
            'delete' => array()
        );

        $this->setMethod($method);
        $this->setTimeout($timeout);

        if ($url) {
            $this->setUrl($url);
        }
    }

    /**
     * Set request method
     *
     * @access public
     * @param string $method
     * @return AbstractRequest
     * @throws InvalidMethodException
     */
    public function setMethod(string $method): AbstractRequest
    {
        $method = strtoupper($method);
        $reflection = new \ReflectionClass('\Ling\PhantomJs\Http\RequestInterface');

        if (!$reflection->hasConstant('METHOD_' . $method)) {
            throw new InvalidMethodException(sprintf('Invalid method provided: %s', $method));
        }

        $this->method = $method;
        return $this;
    }

    /**
     * Get request method
     *
     * @access public
     * @return string
     */
    public function getMethod(): string
    {
        return $this->method;
    }

    /**
     * Set timeout period
     *
     * @access public
     * @param int $timeout
     * @return AbstractRequest
     */
    public function setTimeout(int $timeout): AbstractRequest
    {
        $this->settings['resourceTimeout'] = $timeout;
        return $this;
    }

    /**
     * Get timeout period
     *
     * @access public
     * @return int
     */
    public function getTimeout(): int
    {
        if (isset($this->settings['resourceTimeout'])) {
            return $this->settings['resourceTimeout'];
        }
        return 0;
    }

    /**
     * Set page load delay time (seconds).
     *
     * @access public
     * @param int $delay
     * @return AbstractRequest
     */
    public function setDelay(int $delay): AbstractRequest
    {
        $this->delay = $delay;
        return $this;
    }

    /**
     * Get page load delay time (seconds).
     *
     * @access public
     * @return int
     */
    public function getDelay(): int
    {
        return $this->delay;
    }

    /**
     * Set viewport size.
     *
     * @access public
     * @param int $width
     * @param int $height
     * @return AbstractRequest
     */
    public function setViewportSize(int $width, int $height): AbstractRequest
    {
        $this->viewportWidth = $width;
        $this->viewportHeight = $height;
        return $this;
    }

    /**
     * Get viewport width.
     *
     * @access public
     * @return int
     */
    public function getViewportWidth(): int
    {
        return $this->viewportWidth;
    }

    /**
     * Get viewport height.
     *
     * @access public
     * @return int
     */
    public function getViewportHeight(): int
    {
        return $this->viewportHeight;
    }

    /**
     * Set request URL
     *
     * @access public
     * @param string $url
     * @return AbstractRequest
     */
    public function setUrl(string $url): AbstractRequest
    {
        $this->url = $url;
        return $this;
    }

    /**
     * Get request URL
     *  - Assembles query string for GET
     *  and HEAD requests
     *
     * @access public
     * @return string
     */
    public function getUrl(): string
    {
        if (!in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
            return $this->url;
        }
        $url = $this->url;
        if (count($this->data)) {
            $url .= !str_contains($url, '?') ? '?' : '&';
            $url .= http_build_query($this->data);
        }
        return $url;
    }

    /**
     * Get content body
     *  - Returns query string if not GET or HEAD
     *
     * @access public
     * @return string
     */
    public function getBody(): string
    {
        if (in_array($this->getMethod(), array(RequestInterface::METHOD_GET, RequestInterface::METHOD_HEAD))) {
            return '';
        }
        return http_build_query($this->getRequestData());
    }

    /**
     * Set request data
     *
     * @access public
     * @param array $data
     * @return AbstractRequest
     */
    public function setRequestData(array $data): AbstractRequest
    {
        $this->data = $data;
        return $this;
    }

    /**
     * Get request data
     *
     * @access public
     * @param boolean $flat
     * @return array
     */
    public function getRequestData(bool $flat = true): array
    {
        if ($flat) {
            return $this->flattenData($this->data);
        }
        return $this->data;
    }

    /**
     * Set headers
     *
     * @access public
     * @param array $headers
     * @return AbstractRequest
     */
    public function setHeaders(array $headers): AbstractRequest
    {
        $this->headers = $headers;
        return $this;
    }

    /**
     * Add single header
     *
     * @access public
     * @param string $header
     * @param string $value
     * @return AbstractRequest
     */
    public function addHeader(string $header, string $value): AbstractRequest
    {
        $this->headers[$header] = $value;
        return $this;
    }

    /**
     * Merge headers with existing
     *
     * @access public
     * @param array $headers
     * @return AbstractRequest
     */
    public function addHeaders(array $headers): AbstractRequest
    {
        $this->headers = array_merge($this->headers, $headers);
        return $this;
    }

    /**
     * Get request headers
     *
     * @access public
     * @param string $format
     * @return array|string
     */
    public function getHeaders(string $format = 'default'): array|string
    {
        if ($format === 'json') {
            return json_encode($this->headers);
        }
        return $this->headers;
    }

    /**
     * Add single setting
     *
     * @access public
     * @param string $setting
     * @param string $value
     * @return AbstractRequest
     */
    public function addSetting(string $setting, string $value): AbstractRequest
    {
        $this->settings[$setting] = $value;
        return $this;
    }

    /**
     * Get settings
     *
     * @access public
     * @return array
     */
    public function getSettings(): array
    {
        return $this->settings;
    }

    /**
     * Add cookie.
     *
     * @access public
     * @param string $name
     * @param mixed $value
     * @param string $path
     * @param string $domain
     * @param bool $httpOnly (default: true)
     * @param bool $secure (default: false)
     * @param int|null $expires (default: null)
     * @return AbstractRequest
     */
    public function addCookie(string $name, mixed $value, string $path, string $domain, bool $httpOnly = true, bool $secure = false, int $expires = null): AbstractRequest
    {
        $filter = function ($value) {
            return !is_null($value);
        };

        $this->cookies['add'][] = array_filter(array(
            'name' => $name,
            'value' => $value,
            'path' => $path,
            'domain' => $domain,
            'httponly' => $httpOnly,
            'secure' => $secure,
            'expires' => $expires
        ), $filter);

        return $this;
    }

    /**
     * Delete cookie.
     *
     * @access public
     * @param string $name
     * @return AbstractRequest
     */
    public function deleteCookie(string $name): AbstractRequest
    {
        $this->cookies['delete'][] = $name;

        return $this;
    }

    /**
     * Get cookies
     *
     * @access public
     * @return array
     */
    public function getCookies(): array
    {
        return $this->cookies;
    }

    /**
     * Set body styles
     *
     * @access public
     * @param array $styles
     * @return AbstractRequest
     */
    public function setBodyStyles(array $styles): AbstractRequest
    {
        $this->bodyStyles = $styles;
        return $this;
    }

    /**
     * Get body styles
     *
     * @access public
     * @param string $format (default: 'default')
     * @return array|string
     */
    public function getBodyStyles(string $format = 'default'): array|string
    {
        if ($format === 'json') {
            return json_encode($this->bodyStyles);
        }
        return $this->bodyStyles;
    }

    /**
     * Flatten data into single
     * dimensional array
     *
     * @access protected
     * @param array $data
     * @param string $prefix
     * @param string $format
     * @return array
     */
    protected function flattenData(array $data, string $prefix = '', string $format = '%s'): array
    {
        $flat = array();
        foreach ($data as $name => $value) {
            $ref = $prefix . sprintf($format, $name);
            if (is_array($value)) {
                $flat += $this->flattenData($value, $ref, '[%s]');
                continue;
            }
            $flat[$ref] = $value;
        }
        return $flat;
    }
}
